原始题目:剑指 Offer 41. 数据流中的中位数 (opens new window)
解题思路:
中位数又叫中值,是按顺序排列的一组数据中居于中间位置的数。
因为需要顺序排列,所以我们需要对数据流中的数字进行排序。
自定义规则
以中位数为界限,我们将小于中位数的数字成为较小数,大于中位数的数字成为较大数。较小数的那部分称为较小部分,较大数的部分称为较大部分。然后规定添加过程中,要保持较大部分的个数大于等于较小部分的个数。
我们在计算中位数的时候,需要分为两种情况,元素总个数为奇数和偶数:
- 元素总数为奇数时,需要拿到较大部分中的最小数作为中位数返回;
- 元素总数为偶数时,需要拿到较大部分的最小数和叫小部分的最大数的平均数返回;
数据结构选型
接下来就是思考如何存储较小部分,然后可以快速拿到较小部分的最大数,以及如何存储较大部分,然后可以快速拿到较大部分的最小数?
- 可以使用大根堆来存储较小部分,因为大根堆的根是所有堆内元素最大的数字;
- 可以使用小根堆来存储较大部分,因为小根堆的根是所有堆内元素最小的数字;
思路梳理
- 将数据流的数据,分为较大部分(用小根堆 存储)和较小部分(用大根堆 存储)。
- 添加数字 的时候
- 如果此时总数为偶数,那么根据前面的规定,添加后 要比 多一个数字,先将 添加到 中,然后将 的根添加到 中。
- 如果此时总数为奇数,此时 ,那么为了维持 和 的大小平衡,添加后 和 的个数要想等,先将 添加到 中,然后将 的根添加到 中。
- 总数加一。
- 求中位数时
- 如果总数为奇数,那么直接返回 的根。
- 如果总数为偶数,那么返回 和 的根的平均数。
代码:
class MedianFinder {
/**
* 小顶堆存放的是数据流排序后的后半部分,即较大的一半
*/
private final PriorityQueue<Integer> minHeap;
/**
* 大顶堆存放的是数据流排序后的前半部分,即较小的一半
*/
private final PriorityQueue<Integer> maxHeap;
private int total = 0;
/**
* initialize your data structure here.
*/
public MedianFinder() {
minHeap = new PriorityQueue<>(Comparator.naturalOrder());
maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
}
/**
* 添加的过程中,要保持小顶堆的元素个数大于等于大顶堆的个数,
* 即总元素个数为奇数的时候,小顶堆要比大顶堆多一个元素
*/
public void addNum(int num) {
// 如果是偶数,则入小顶堆,但是不能直接入
// 原因是,这个数字可能属于较小的一半,所以需要先入大顶堆,再把大顶堆的根弹出
if ((total & 1) == 0) {
maxHeap.offer(num);
minHeap.offer(maxHeap.poll());
} else {
// 如果是奇数,则入大顶堆,但是不能直接入
// 原因是,这个数字可能属于较大的一半,所以需要先入小顶堆,再把小顶堆的根弹出
minHeap.offer(num);
maxHeap.offer(minHeap.poll());
}
total++;
}
public double findMedian() {
// 偶数则返回中间两个元素的和的一半
if ((total & 1) == 0) {
return (double) (minHeap.peek() + maxHeap.peek()) / 2;
} else {
// 奇数则返回小顶堆的根
return (double) minHeap.peek();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
复杂度分析
时间复杂度
- 添加数字 :堆的弹出和压入使用 的时间;
- 求中位数:获取堆顶元素使用 时间。
空间复杂度:其中 为数据流中的元素数量,小顶堆 和大顶堆 最多同时保存 个元素。